一、前言

现在的项目中很多地方都会用到文件上传,不说百分百,但是绝对会占百分之九十(你可以反驳,但是我不接受),如果我们单纯的采用整个文件上传的方式,在遇到大文件就会出现各种花式问题,为了尽量避免这些坑,我们可以采用分片上传的方式将大文件分成不同的小文件进行上传

二、新建文件上传所需的VO文件

1、新建 CheckMd5FileVO 文件以及 UploadVO 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
* 文件MD5校验VO
*/
@Data
@ApiModel("文件MD5校验VO")
public class CheckMd5FileVO {

/**
* 0:不分片,1:分片
*/
@ApiModelProperty(value = "分片状态 0:不分片,1:分片", required = true)
private Integer type;
/**
* 文件名
*/
@ApiModelProperty(value = "文件名", required = true)
private String fileName;

/**
* 文件Md5(文件唯一表示)
*/
@ApiModelProperty(value = "文件Md5(文件唯一表示)", required = true)
private String fileMd5;

/**
* 当前分片下标
*/
@ApiModelProperty(value = "当前分片下标", required = true)
private Long chunk;

/**
* 文件大小(如果分片了,则是分片文件大小)
*/
@ApiModelProperty(value = "文件大小 如果分片了,则是分片文件大小", required = true)
private Long fileSize;

private String formData;

@ApiModelProperty(value = "文件后缀名", required = true)
public String getSuffix() {
return fileName.substring(fileName.lastIndexOf(".") + 1);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.Date;

/**
* 文件上传VO
*/
@Data
@ApiModel("文件上传VO")
public class UploadVO {

/***
* 文件id WU_FILE_0
*
*/
private String id;
/**
* 文件名称 Beyond Compare.rar
*/
@ApiModelProperty(value = "文件名称", required = true)
private String name;
/**
* 类型 application/octet-stream
*/
private String type;
/**
* 文件大小
*/
private Long size;

/**
* 最后修改时间
*/
private Date lastModifiedDate;

/**
* 分片片数
*/
@ApiModelProperty(value = "分片片数", required = true)
private Long chunks;

/**
* 当前分片标识
*/
@ApiModelProperty(value = "当前分片标识", required = true)
private Long chunk;

/**
* 分片设置大小
*/
@ApiModelProperty(value = "分片设置大小", required = true)
private Long chunkSize;

/**
* 表单数据
*/
private String formData;

/**
* 文件Md5(文件的唯一标识)
*/
@ApiModelProperty(value = "文件md5值", required = true)
private String fileMd5;

@ApiModelProperty(value = "文件名后缀", required = true)
public String getSuffix() {
return name.substring(name.lastIndexOf(".") + 1);
}
}

三、新建 FileUtil 工具类

该工具类主要是为了处理分片文件的合并和临时文件的清理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.nio.channels.FileChannel;
import java.util.*;


@Slf4j
public class FileUtil {


public static void randomAccessFile(File in, File out, Long seek) throws IOException {
RandomAccessFile raFile = null;
BufferedInputStream inputStream = null;
try {
// 以读写的方式打开目标文件
raFile = new RandomAccessFile(out, "rw");
raFile.seek(seek);
inputStream = new BufferedInputStream(new FileInputStream(in));
byte[] buf = new byte[1024];
int length = 0;
while ((length = inputStream.read(buf)) != -1) {
raFile.write(buf, 0, length);
}
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (raFile != null) {
raFile.close();
}
} catch (Exception e) {
throw new IOException(e.getMessage());
}
}
}


/**
* 删除单个文件
*
* @param sPath 被删除文件的文件名
* @return 单个文件删除成功返回true,否则返回false
*/
public static boolean deleteFile(String sPath) {
boolean flag = false;
File file = new File(sPath);
// 路径为文件且不为空则进行删除
if (file.isFile() && file.exists()) {
file.delete();
flag = true;
}
return flag;
}


/**
* 删除目录(文件夹)以及目录下的文件
*
* @param sPath 被删除目录的文件路径
* @return 目录删除成功返回true,否则返回false
*/
public static boolean deleteDirectory(String sPath) {
// 如果sPath不以文件分隔符结尾,自动添加文件分隔符
if (!sPath.endsWith(File.separator)) {
sPath = sPath + File.separator;
}
File dirFile = new File(sPath);
// 如果dir对应的文件不存在,或者不是一个目录,则退出
if (!dirFile.exists() || !dirFile.isDirectory()) {
return false;
}
boolean flag = true;
// 删除文件夹下的所有文件(包括子目录)
File[] files = dirFile.listFiles();
for (int i = 0; i < files.length; i++) {
// 删除子文件
if (files[i].isFile()) {
flag = deleteFile(files[i].getAbsolutePath());
if (!flag)
break;
} // 删除子目录
else {
flag = deleteDirectory(files[i].getAbsolutePath());
if (!flag)
break;
}
}
if (!flag)
return false;
// 删除当前目录
if (dirFile.delete()) {
return true;
} else {
return false;
}
}
}

四、新建 ChunkUploadService

service 处理文件分片检查、文件上传等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import com.boran.demonstration.config.CustomConfig;
import com.boran.demonstration.config.Result;
import com.boran.demonstration.enums.ErrorCodeEnum;
import com.boran.demonstration.upload.util.FileUtil;
import com.boran.demonstration.upload.vo.CheckMd5FileVO;
import com.boran.demonstration.upload.vo.UploadVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
* 代码中对应的以 CustomConfig.xxxx 的自行更换为自己对应的文件路径
* 代码中出现的 ErrorCodeEnum.xxxx 的自行更换为自己对应的错误提示码
*/
@Service
@Slf4j
public class ChunkUploadService {


private static final String DELIMITER = "-";


/**
* 上传之前校验(整个文件、分片)
*
* @param md5FileVO
* @return
*/
public Result check(CheckMd5FileVO md5FileVO) {
if (md5FileVO.getType() == null || md5FileVO.getChunk() == null || md5FileVO.getFileMd5() == null || md5FileVO.getSuffix() == null || md5FileVO.getFileName() == null) {
return Result.error(ErrorCodeEnum.MISSING_REQUIRED_ARGUMENTS);
}
Integer type = md5FileVO.getType();
Long chunk = md5FileVO.getChunk();
String fileName = md5FileVO.getFileMd5() + "." + md5FileVO.getSuffix();
Long fileSize = md5FileVO.getFileSize();
if (type == 0) {// 未分片校验
String destFilePath = CustomConfig.fileSave + File.separator + fileName;
File destFile = new File(destFilePath);
if (destFile.exists() && destFile.length() == fileSize) {
Map<String, Object> map = new HashMap<>();
map.put("url", fileName);
map.put("size", fileSize);
return Result.ok(map);
} else {
return Result.error(ErrorCodeEnum.FILE_NOT_EXISTS);
}
} else {// 分片校验
String fileMd5 = md5FileVO.getFileMd5();
String destFileDir = CustomConfig.fileSave + File.separator + fileMd5;
String destFileName = chunk + DELIMITER + fileName;
String destFilePath = destFileDir + File.separator + destFileName;
File destFile = new File(destFilePath);
if (destFile.exists() && destFile.length() == fileSize) {
return Result.error(ErrorCodeEnum.CHUNK_EXISTS);
} else {
return Result.error(ErrorCodeEnum.CHUNK_NOT_EXISTS);
}
}
}

/**
* 分片上传
*
* @param file
* @param uploadVO
* @return
*/
public Result ChunkUploads(MultipartFile file, UploadVO uploadVO) {
String fileMd5 = uploadVO.getFileMd5();
String fileName = fileMd5 + "." + uploadVO.getSuffix();
Long chunk = uploadVO.getChunk();// 当前片
Long chunks = uploadVO.getChunks();// 总共多少片

// 分片目录创建
String chunkDirPath = CustomConfig.fileDispose + File.separator + fileMd5;
File chunkDir = new File(chunkDirPath);
if (!chunkDir.exists()) {
chunkDir.mkdirs();
}
// 分片文件上传
String chunkFileName = chunk + DELIMITER + fileName;
String chunkFilePath = chunkDir + File.separator + chunkFileName;
File chunkFile = new File(chunkFilePath);
try {
file.transferTo(chunkFile);
} catch (Exception e) {
log.error("分片上传出错", e);
return Result.error(ErrorCodeEnum.CHUNK_UPLOAD_ERROR);
}
// 合并分片
Long chunkSize = uploadVO.getChunkSize();
long seek = chunkSize * chunk;
String destFilePath = CustomConfig.fileSave + File.separator + fileName;
File destFile = new File(destFilePath);
if (chunkFile.length() > 0) {
try {
System.out.println("合并中......");
FileUtil.randomAccessFile(chunkFile, destFile, seek);
} catch (IOException e) {
log.error("分片{}合并失败:{}", chunkFile.getName(), e.getMessage());
return Result.error(ErrorCodeEnum.CHUNK_MERGE_FAIL);
}
}
if (chunk == chunks - 1) {
// 删除分片文件夹
System.out.println("删除分片文件夹......");
FileUtil.deleteDirectory(chunkDirPath);
Map<String, Object> map = new HashMap<>();
map.put("url", fileName);
return Result.ok(map);
} else {
return Result.error(ErrorCodeEnum.UPLOADING);
}
}


/**
* 未分片上传
*
* @param file
* @param uploadVO
* @return
*/
public Result UnChunkUploads(MultipartFile file, UploadVO uploadVO) {
String suffix = uploadVO.getSuffix();
String fileName = uploadVO.getFileMd5() + "." + suffix;
// 文件上传
File destFile = new File(CustomConfig.fileSave + File.separator + fileName);
if (file != null && !file.isEmpty()) {
// 上传目录
File fileDir = new File(CustomConfig.fileSave);
if (!fileDir.exists()) {
fileDir.mkdirs();
}
if (destFile.exists()) {
destFile.delete();
}
try {
file.transferTo(destFile);
Map<String, Object> map = new HashMap<>();
map.put("url", fileName);
return Result.ok(map);
} catch (Exception e) {
log.error("文件上传出错", e);
return Result.error(ErrorCodeEnum.FILE_UPLOAD_ERROR);
}
}
return Result.error(ErrorCodeEnum.UPLOAD_FAIL);
}

}

五、创建controller

新建接口,一个用于文件分片检查,一个用于文件上传

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import com.boran.demonstration.config.BaseController;
import com.boran.demonstration.config.Result;
import com.boran.demonstration.upload.service.ChunkUploadService;
import com.boran.demonstration.upload.vo.CheckMd5FileVO;
import com.boran.demonstration.upload.vo.UploadVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Api(tags = "文件上传")
@CrossOrigin
@RestController
@RequestMapping("/file")
public class FileController {

@Autowired
private ChunkUploadService chunkUploadService;


/**
* 文件检查
*
* @param md5FileVO
* @return
*/
@ApiOperation("文件检查")
@PostMapping("check")
public Result check(CheckMd5FileVO md5FileVO) {
return chunkUploadService.check(md5FileVO);
}


/**
* 上传文件
*
* @param file
* @param uploadVO
* @return
*/
@ApiOperation("上传文件")
@PostMapping("/save")
public Result save(@RequestParam("file") MultipartFile file, UploadVO uploadVO) {
Long chunk = uploadVO.getChunk();
if (chunk == null) {// 没有分片
return chunkUploadService.UnChunkUploads(file, uploadVO);
} else {// 分片
return chunkUploadService.ChunkUploads(file, uploadVO);
}

}
}

六、结语

相关功能的测试结果就不贴了,有兴趣可以自己测试一下,不出意外的话,是没有问题的,当然了,出了问题概不负责,哈哈哈哈哈哈……